// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.

//
//  BGMGooglePlayMusicDesktopPlayer.m
//  BGMApp
//
//  Copyright © 2019 Kyle Neideck
//

// Self Include
#import "BGMGooglePlayMusicDesktopPlayer.h"

// Local Includes
#import "BGM_Types.h"
#import "BGM_Utils.h"
#import "BGMAppWatcher.h"
#import "BGMGooglePlayMusicDesktopPlayerConnection.h"

// PublicUtility Includes
#import "CADebugMacros.h"


#pragma clang assume_nonnull begin

@implementation BGMGooglePlayMusicDesktopPlayer {
    BGMUserDefaults* userDefaults;
    BGMGooglePlayMusicDesktopPlayerConnection* connection;
    BGMAppWatcher* appWatcher;

    // True while the auth code dialog is open. The user types in the four-digit auth code from
    // GPMDP when we connect to it for the first time.
    BOOL showingAuthCodeDialog;
    // True if the user has cancelled the auth code dialog. We only show the auth code dialog again
    // after the user has changed the music player and then changed it back to GPMDP (or restarted
    // BGMApp).
    BOOL authCancelled;
}

+ (NSArray<id<BGMMusicPlayer>>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults {
    return @[[[self alloc] initWithUserDefaults:userDefaults]];
}

- (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults {
    // If you're copying this class, replace the ID string with a new one generated by uuidgen (the
    // command line tool).
    NSUUID* playerID = [BGMMusicPlayerBase makeID:@"FCDCC01F-4BF1-4AD2-BE3E-6B7659A90A3F"];
    if ((self = [super initWithMusicPlayerID:playerID
                                        name:@"GPMDP"
                                     toolTip:@"Google Play Music Desktop Player"
                                    bundleID:@"google-play-music-desktop-player"])) {
        userDefaults = defaults;
        showingAuthCodeDialog = NO;
        authCancelled = NO;

        // We don't strictly need to use a weak ref (at least not yet), but it doesn't hurt.
        BGMGooglePlayMusicDesktopPlayer* __weak weakSelf = self;

        connection = [[BGMGooglePlayMusicDesktopPlayerConnection alloc]
                initWithUserDefaults:userDefaults
                 authRequiredHandler:^{
                     BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
                     return [strongSelf requestAuthCodeFromUser];
                 }
              connectionErrorHandler:^{
                  BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
                  [strongSelf showConnectionErrorDialog];
              }
           apiVersionMismatchHandler:^(NSString* reportedAPIVersion) {
               BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
               [strongSelf showAPIVersionMismatchDialog:reportedAPIVersion];
           }];

        // Set up callbacks that run when GPMDP is opened or closed.
        appWatcher = [[BGMAppWatcher alloc]
                initWithBundleID:BGMNN(self.bundleID)
                     appLaunched:^{
                         BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
                         [strongSelf gpmdpWasLaunched];
                     }
                   appTerminated:^{
                       BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf;
                       [strongSelf gpmdpWasTerminated];
                   }];
    }
    
    return self;
}

- (void) gpmdpWasLaunched {
    if (self.selected) {
        // Reconnect so we can control GPMDP.
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasLaunched: GPMDP launched. Connecting");

        // Try up to 10 times because GPMDP won't start accepting connections until it's finished
        // starting up.
        //
        // TODO: If GPMDP shows an alert before it finishes launching, it doesn't start accepting
        //       connections until the alert is dismissed, which can make this can timeout.
        // TODO: Is the error dialog still shown if the user closes GPMDP again while we're
        //       retrying? It shouldn't be.
        [connection connectWithRetries:10];
    }
}

- (void) gpmdpWasTerminated {
    if (self.selected) {
        // Allow the connection to clean up and reset itself.
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasTerminated: GPMDP has been closed.");
        [connection disconnect];
    }
}

- (void) wasSelected {
    [super wasSelected];

    // Allow the auth code dialog to be shown again if we were hiding it because the user cancelled
    // it last time.
    authCancelled = NO;

    if (self.running) {
        // Only retry once so the error message is shown fairly quickly if we fail to connect.
        [connection connectWithRetries:1];
    }
}

- (void) wasDeselected {
    [super wasDeselected];
    [connection disconnect];
}

- (NSString* __nullable) requestAuthCodeFromUser {
    if (showingAuthCodeDialog) {
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
                 "Already showing the auth code dialog");
        return nil;
    }

    if (authCancelled) {
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: "
                 "Previously cancelled. Doing nothing.");
        return nil;
    }

    showingAuthCodeDialog = YES;

    // Ask the user to read the auth code from GPMDP and type it in to BGMApp.
    NSString* __nullable authCode = [self showAuthCodeDialog];

    showingAuthCodeDialog = NO;

    return authCode;
}

- (NSString* __nullable) showAuthCodeDialog {
    // When this isn't being called because the user just changed something in BGMApp (e.g. GPMDP
    // was closed, they selected it in BGMApp for the first time, then opened GPMDP later), we could
    // use notifications instead of an NSAlert. But it probably wouldn't happen often enough to be
    // worth the effort.
    NSAlert* alert = [NSAlert new];
    alert.messageText = @"Background Music needs permission to control GPMDP.";
    alert.informativeText = @"It should be displaying a four-digit code for you to enter.";
    [alert addButtonWithTitle:@"OK"];
    [alert addButtonWithTitle:@"Cancel"];

    // The text field to type the auth code in.
    // TODO: Can we derive these dimensions from something instead of hardcoding them?
    NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 50, 24)];
    [alert setAccessoryView:input];

    // Focus the text field (so the user doesn't have to do it themselves).
    [alert.window setInitialFirstResponder:input];

    // Bring GMPDP to the front, underneath our NSAlert, so the user can see the auth code.
    [self showGPMDPBehindAuthCodeDialog];

    NSModalResponse buttonPressed = [alert runModal];

    if (buttonPressed == NSAlertFirstButtonReturn) {
        // Set input's value to the text entered by the user so we can access it.
        [input validateEditing];

        DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: Got auth code: <private>");
        return input.stringValue;
    } else {
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: "
                 "The user cancelled the auth code dialog");
        authCancelled = YES;
        return nil;
    }
}

- (void) showGPMDPBehindAuthCodeDialog {
    // Dispatched because if we do this just before showing the auth code dialog, the user's current
    // active window will be deactivated, the auth code dialog will become the active window and
    // macOS will act as if the user activated it themselves. To avoid stealing key focus, it won't
    // activate GPMDP.
    //
    // We could pass NSApplicationActivateIgnoringOtherApps to activateWithOptions instead, but then
    // GPMDP would be activated even if the user really did activate a different application, which
    // would steal focus from it.
    //
    // 250 ms is a reasonable value on my system, but won't always be long enough. When it isn't,
    // GPMDP won't be activated, but that just means the user will have to do it themselves.
    const int64_t delay = 250 * NSEC_PER_MSEC;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
                   dispatch_get_main_queue(),
                   ^{
                       // Make GMPDP the frontmost app.
                       NSArray<NSRunningApplication*>* gpmdpApps =
                           [NSRunningApplication
                            runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];

                       if (gpmdpApps.count > 0) {
                           [gpmdpApps[0] activateWithOptions:0];
                       }

                       // Focus the auth code dialog. It will already be in front of GPMDP because
                       // it's modal. Dispatched for the same reason as above.
                       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay),
                                      dispatch_get_main_queue(),
                                      ^{
                                          [NSApp activateIgnoringOtherApps:YES];
                                      });
                   });
}

- (void) showConnectionErrorDialog {
    NSString* errorMsg = @"Could not connect to Google Play Music Desktop Player";
    NSString* troubleshootingMsg =
        [NSString stringWithFormat:
         @"Make sure \"Enable JSON API\" and \"Enable Playback API\" are both checked in GPMDP's "
         "settings, then restart GPMDP.\n\n"
         "GPMDP should be listening on its default port, 5672.\n\n"
         "Consider filing a bug report at %s",
         kBGMIssueTrackerURL];

    [self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
}

- (void) showAPIVersionMismatchDialog:(NSString*)reportedAPIVersion {
    NSString* errorMsg = @"Google Play Music Desktop Player Version Not Supported";
    NSString* troubleshootingMsg =
            [NSString stringWithFormat:
             @"GPMDP reported its API version as \"%@\", which Background Music doesn't support "
             "yet. Background Music might not be able to control GPMDP properly.\n\n"
             "Feel free to open an issue about this at %s",
             reportedAPIVersion,
             kBGMIssueTrackerURL];

    [self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg];
}

- (void) showErrorDialog:(NSString*)errorMsg troubleshootingMsg:(NSString*)troubleshootingMsg {
    if (!self.running) {
        // GPMDP isn't running, so there's no need to inform the user. (The "Auto-pause GPMDP" menu
        // item will be greyed out, but that's handled elsewhere.)
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::showErrorDialog: Not running");
        return;
    }

    NSLog(@"%@", errorMsg);

    // Show the error in a UI dialog.
    NSAlert* alert = [NSAlert new];
    alert.messageText = errorMsg;
    alert.informativeText = troubleshootingMsg;
    // TODO: Show the suppression checkbox and save its value in user defaults.
    alert.showsSuppressionButton = NO;
    [alert addButtonWithTitle:@"OK"];

    [alert runModal];
}

- (BOOL) isRunning {
    // We have to check with NSRunningApplication instead of just setting a flag in appWatcher's
    // callbacks because BGMAutoPauseMenuItem calls this method when it's notified by its own
    // instance of BGMAppWatcher. If BGMAutoPauseMenuItem got notified first, the flag wouldn't be
    // updated in time.
    //
    // At some point we might want to try to avoid this by making the BGMMusicPlayers' running
    // properties observable.
    NSArray<NSRunningApplication*>* instances =
        [NSRunningApplication runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)];

    return instances.count > 0;
}

- (BOOL) isPlaying {
    return self.running && connection.playing;
}

- (BOOL) isPaused {
    return self.running && connection.paused;
}

- (BOOL) pause {
    // isPlaying checks isRunning, so we don't need to check it here.
    BOOL wasPlaying = self.playing;
    
    if (wasPlaying) {
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::pause: Pausing Google Play Music Desktop "
                 "Player");
        // There's a race condition here and in unpause because, if the user paused GPMDP just
        // before we called playPause, GPMDP would play instead of pausing. I'm not sure there's
        // much we can/should do about it.
        [connection playPause];
    }
    
    return wasPlaying;
}

- (BOOL) unpause {
    // isPaused checks isRunning, so we don't need to check it here.
    BOOL wasPaused = self.paused;
    
    if (wasPaused) {
        DebugMsg("BGMGooglePlayMusicDesktopPlayer::unpause: Unpausing Google Play Music Desktop "
                 "Player");
        [connection playPause];
    }
    
    return wasPaused;
}

@end

#pragma clang assume_nonnull end

